В статье подробно рассмотрен новый пакет компонентов (Vlad Karpov Native Link to ORACLE – VK NLO), предоставляющий возможность проектировать клиентское приложение для сервера ORACLE 8 в среде DELPHI (4, 5) или C++ Builder (4, 5). Эти компоненты изначально разрабатывались автором статьи для собственных нужд, но постепенно переродились в нечто большее - в продукт, полностью пригодный для применения и выгодно отличающийся от других подобных. Основные отличительные черты - это простота применения и быстродействие, которое, в отдельных случаях, на несколько порядков превосходит аналоги. Компоненты написаны с использованием ORACLE CALL INTERFACE (OCI), в связи с чем обладают поразительной гибкостью при работе с сервером. Кроме того, подход автора к OCI гарантирует наиболее полное использование ресурсов этого инструмента.
Карпов Владислав, TALGAR
То, что Вам предлагается автором статьи как средство разработки клиентских приложений на DELPHI для сервера ORACLE, является плодом трехлетней работы одного человека, некоторого количества бессонных ночей, примерно годом раздумий, годом написания кода и годом отладки в Debug-ере этого кода.
Я прекрасно понимаю, что давать советы в этой жизни - самое простое занятие, но позвольте все-таки дать один. Не пытайтесь проделать тоже самое, не надо...
Когда я только начинал этим заниматься, я преследовал вполне определенные цели, весьма далекие от самой концепции получения инструмента общего пользования или, не дай Бог, коммерческого продукта. Мне пришлось этим заниматься, так как существующие решения не совсем меня удовлетворяли, или я о них просто ничего не знал. Вообще начиналось все с ORACLE 7. Я тогда программировал на замечательном языке под названием CA-Visual Objects. Эта система прекрасно работала с DBF таблицами, но совершенно отвратно с SQL серверами вообще, и с ORACLE в частности, а мне просто было надо работать с ним именно оттуда.
Так или иначе я проникся идеями программирования сервера ORACLE при помощи ORACLE CALL INTERFACE (OCI) и уже не мог от них отступить. Зная достаточно хорошо его возможности, я никак не мог смириться с тем, что большинство программных решений, особенно универсальных, используют лишь маленькую часть потенциала OCI.
Кроме того, в дальнейшем выяснилось, что эти программные решения не лишены определенного количества ошибок, которые в 100% случаев не удается исправить из-за недоступности кода. Таким образом, ошибки самого OCI накладываются на ошибки этого промежуточного звена, на которые я никак не могу влиять, и это удручало.
Но ведь можно было воспользоваться уже написанными аналогичными компонентами, - скажете Вы.
Да, можно. Но три года назад для меня это было совсем не очевидно.
В принципе, и тогда я подозревал, что изобретаю велосипед, но со временем у моего велосипеда появились замечательные свойства - как чисто технические решения, так и принципиальные вещи для меня лично и для других разработчиков.
Дело в том, что у своего велосипеда я всегда на ходу поменяю все спицы при необходимости, и я всегда знаю, как далеко он может уехать.
VCL схема компонент представлена Вашему вниманию на рисунке ниже.
Черными стрелками показаны родственные отношения в иерархии классов, синими - партнерские отношения. Партнерские отношения - это когда в компоненте присутствует свойство с типом другого компонента. Например, Statement может выполняться только в контексте какой-то ORACLE Session, поэтому компонент To8Stmt должен содержать ссылку на компонент To8Session.
Приглядимс к схеме повнимательней. Она естественным образом отражает схему хендлеров OCI. OCI написан на C, C не объектно-ориентированный язык. Однако объекты можно смоделировать в C посредством некоторых уникальных идентификаторов, характеризующих объект. Это стандартный подход. В Windows API принята, например, та же технология. Эти уникальные идентификаторы объектов в OCI (и не только) называются хендлерами (handler) или дескрипторами (descriptor) и в действительности являются просто указателями на объект, а точнее, на некоторую структуру, его описывающую. Причем в OCI даже не уточняется, что это за структура - это неважно. Если раскопать до конца определение Environment хендлера, то Вы получите указатель *VOID, т.е ссылку в "никуда". Хотя это "никуда" становится вполне реально выделенной памятью после OCI вызова OCIHandleAlloc.
Схема хендлеров и дескрипторов OCI представлена на диаграмме:
Во главе всего находится Environment хендлер. Этот хендлер отвечает за среду, в которой будет жить Ваше OCI приложение. Естественно, в VCL схеме имеетс компонент, отвечающий за инициализацию и уничтожение Environment хендлера - это To8Env компонент.
Понятие ORACLE сессии в OCI равномерно размазано по 3 хендлерам, - OCIServer, OCISession, OCISvcCtx.
OCIServer - сервер хендлер, отвечает за коннект к серверу.
OCISession, - ORACLE сессия.
OCISvcCtx, - сервис контекст хендлер. Является входным параметром для всех OCI процедур, которые непосредственно выполняют SQL/PL-SQL. Выполнение происходит как бы в некотором контексте сервиса, предоставляемого ORACLE.
Все три вышеперечисленных хендлера связаны в VCL компоненте To8Session. Этот компонент устанавливает коннект к серверу, открывает ORACLE сессию и инициализирует публикуемый для компонентов, исполняющих SQL/PL-SQL, сервис контекст хендлер. Для работы To8Session необходим Environment хендлер, поэтому у компонента есть ссылка на To8Env.
Дл выполнения SQL/PL-SQL можно придумать много разных интересных подходов. Наиболее распространенные - это Table, Query и StoredProc. Но у всех подходов четко прослеживаются общие черты. Все они должны передать параметры серверу вместе с SQL/PL-SQL предложением или программой, выполнить это предложение или программу, принять выходные параметры, принять и уметь обрабатывать результирующую выборку (Result Set).
Именно по этой причине мной был разработан базовый абстрактный класс To8Stmt, задающий общие вызовы и свойства всех исполняющих компонентов.
Единственный на сегодняшний момент компонент - наследник от To8Stmt, исполняющий SQL/PL-SQL - это To8StmtOrd, реализующий хендлеры OCIStmt, OCIBind, OCIDefine. Для работы ему необходим сервис контекст хендлер и Environment хендлер, которые он получает из To8Session. Ссылка на этот компонент прописана еще в базовом классе To8Stmt.
На схеме голубым цветом помечены другие исполняющие компоненты, которые в данный момент не реализованы, а только планируются, и сделаны будут скорее всего на базе компонента To8StmtOrd, после того как я приделаю к нему работу с объектами и хорошо его отлажу.
Дл удобного представления и редактирования данных из Result Set в VCL уже давно продумана своя технология, которая, как я считаю, является самой передовой на сегодняшний момент и такой останется еще очень долго. Для того, чтобы Ваш собственный источник данных стал частью этой технологии, нужно просто наследовать его от базового VCL класса TDataSet. Именно это я и сделал, написав аж 2 потомка от TDataSet - To8DataSet и To8RODataSet. Первый предназначен дл редактирования Result Set, для чего ест немало оперативной памяти (весь Result Set со всеми модификациями пользователя он хранит во внутреннем TList). Второй предназначен только для просмотра, и вообще не ест никакой памяти. Для обоих компонентов первоначальным источником данных является любой наследник от To8Stmt.
Не могу сказать, что это сильно удобно, из-за дополнительного звена между исполнителем SQL и конечным зависящим от данных управляющим элементом (DB-aware control). Но это звено дает чрезвычайную гибкость при построении приложения. (Недаром весь мир медленно, но верно сползает на трехзвенку).
Компонент To8DataSet хранит все изменения Result Set, сделанные пользователем. Следующим шагом является отправка этих изменений на сервер и фиксация их в базе (commit). В VCL библиотеке специально для этой операции существует компонент TUpdateSQL. По сути, это просто диспетчер всех изменений на клиенте по трем "условным" операциям на сервере - INSERT, DELETE и UPDATE. "Условным", потому что это могут быть как просто команды SQL, так и какие-то сложные операции, написанные на PL/SQL. Механизм этот называется Cached updates - кэшируемые изменения, и, с моей точки зрения, просто гениален. Естественно, я написал свой To8UpdateSQL, который работает через Call Interface, вешается на To8DataSet и выполняет вышеописанные функции.
Компонент To8Script очень прост и предназначен для выполнения SQL скриптов (последовательность SQL операторов, разделенных символом '/' в новой строке после оператора). Сам он не занимается выполнением, а делает элементарный лексический разбор скрипта и передает каждое отдельное SQL предложение дл выполнения компоненту To8StmtOrd.
В своей повседневной деятельности в качестве прикладного программиста я очень часто сталкивался с проблемой миграции данных. Причем мигрировали они у мен исключительно только в ORACLE, и это понятно: познакомившись с возможностями этого сервера, ни с чем другим не хочется работать. Для этой цели я когда-то написал целую программу, которую демонстрировал на конференции "Техникон-98" , но это было очень давно, а сейчас у меня есть специальный компонент TDSet2Ora (Data Set to Oracle). Занимается он буквально следующим: берет ЛЮБОЙ DataSet (TQuery, TTable, TStoredProc) и с бешеной скоростью засасывает его в таблицу ORACLE. Скорость работы сравнима со скоростью работы ORACLE SQL*Loader и достигается за счет применения механизма OCI под названием Array of structures (массив структур). Механизм этот позволяет делать, например, один INSERT на сервере с целым массивом параметров, а не с одним их набором, что сильно уменьшает сетевой трафик и увеличивает скорость исполнения. Исключение составляют таблицы с MEMO и BLOB полями, которые приходится грузить по одной записи.
Существует еще несколько классов, которые не показаны на схеме. Среди них TOraField и TOraParam, назначение которых - управление полями Result Set и параметрами дл SQL/PL-SQL предложений соответственно.
Компонент TDBFDataSet непосредственного отношения о ORACLE не имеет. Это просто Data Set, который понимает DBF таблицы, причем только на чтение, без индексов и мемо-полей. Сделан он был мной с одной-единственной целью: засосать DBF в ORACLE и там с ним работать.
Конечно, я понимаю, что есть BDE и куча других компонентов, работающих с DBF. Но интересная, однако, у меня сложилась ситуация. Мне потребовалось перекачать в ORACLE все таблицы из бухгалтерской системы Инфин, написанной на DOS-овском CLIPPER-е и работающей с DBF-ами. Все бы было замечательно, но в CLIPPER-е можно установить отношение между таблицами не по полям (как это прописано в любом учебнике по теории баз данных), а по физическому номеру записи в DBF таблице. Вот так вот: SET RELATION TO RECNO и ... все замечательно. Кроме того, в DBF таблице записи физически удаляются только после специальной операции PACK, а в рабочем режиме они просто помечаются как удаленные. В связи с тем что Инфин изобилует отношениями по номеру записи, операция PACK там не делается нигде принципиально из-за соображений целостности данных. Из всего вышесказанного следует, что для того, чтобы перекачать Инфин в ORACLE, нужно для каждой таблицы определить два лишних вычисляемых поля - RecNo и Deleted. Если работать через BDE, то с RecNo проблем не возникает, а вот с Deleted... Я так и не нашел способа, как убедить BDE показывать записи, помеченные на удаление. Если кто знает, прошу, сообщите, пожалуйста.
Другие компоненты сторонних фирм и разработчиков, как правило, сильно перегружены и избыточны для задачи миграции. К тому же у меня нет к ним особого доверия, и то время, которое я потратил бы на тестирование, я просто потратил на разработку собственного компонента. Короче, за 2 дня я наваял TDBFDataSet и снял с себя все проблемы.
Итак, To8Env. Как я уже говорил, это компонент одного-единственного хендлера OCIEnv. Но не только. Он также служит для загрузки OCI библиотеки и инициализации указателей на OCI функции. При этом происходит выбор библиотеки из текущего ORACLE HOME при помощи маленькой, но хитрой процедуры. Процедуру я эту позаимствовал у Дмитрия Арефьева, такого же разработчика, как и я, написавшего аналогичный набор компонентов NCOCI8 (dmitrya@inthink.com, http://www.ncom.ru/diman/index.html), с его, естественно, согласия.
Компонент имеет единственное published свойство Active логического типа. При установке Active в TRUE и происходят, собственно, все инициализации и загрузка OCI библиотеки (при условии, что она еще не была загружена). После активации компонента из программы доступны два public свойства: OCIDllName хранит полный путь и имя OCI библиотеки; hEnv - инициализированный OCIEnv хендлер.
Поиск OCI библиотеки в автоматическом режиме происходит только в том случае, если свойство OCIDllName пусто на момент присваивания To8Env.Active := TRUE, в противном случае происходит загрузка библиотеки, указанной в OCIDllName. Эта маленькая тонкость позволяет динамически манипулировать программой загрузки DLL.
Хендлеры и дескрипторы в OCI иерархичны, так как иерархичны объекты, которые они представляют. Естественно, что при уничтожении объекта более высокой ступени он уничтожает все объекты, принадлежащие ему. Environment хендлер стоит в вершине графа иерархии хендлеров, поэтому при освобождении самого хендлера освобождаютс как ресурсы, связанные с ним, так и привязанные к нему хендлеры и их ресурсы. Фактически при OCI вызове OCIHandleFree(hEnv, OCI_HTYPE_ENV) освобождаются ВСЕ ресурсы. Компонент спроектирован таким образом, что вызов OCIHandleFree(hEnv, OCI_HTYPE_ENV) происходит при установке свойства Active в FALSE, т.е. при переводе компонента в неактивное состояние. И это очень важно. Приведу пример. Существует класс программ, для которых очень критична проблема утечки памяти. Это могут быть, например, NT сервисы, которые месяцами или даже годами вертятс в недрах какого-либо сервера и чем-то занимаются, реагируя на те или другие события. С целью устранения ошибки утечки памяти целесообразно было бы спроектировать такую программу следующим образом: по некоторому событию сервис инициализирует Environment хендлер, устанавливает коннект, что-то там делает на сервере и уничтожает Environment хендлер, освобождая ВСЕ ресурсы и отсоединяясь от сервера. У меня была такая задача, когда надо было каждую ночь получать несколько бухгалтерских отчетов и отсылать их по электронной почте смежным организациям.
Кроме того, я специальным образом тестировал компоненты на предмет утечки памяти.
Вы можете бросить на форму или Data-модуль несколько компонентов To8Env и активизировать их. Для Вашей программы это будет значить, что внутри нее будут жить несколько OCI приложений. Особой необходимости или какого-то смысла в этом я не вижу.
В заключение разговора о компоненте To8Env скажу, что при переводе его в неактивное состояние он автоматически переводит в неактивное состояние все сессии, связанные с ним, те, в свою очередь, все Statement-ы, Statement-ы - DataSet-ы.
Итак, To8Session. Предназначен главным образом для установки коннекта к серверу и открытия на сервере ORACLE сессии. Управляет следующими хендлерами: Server handle, Session handle, Service context handle, Error handle. При переводе сессии в неактивное состояние все хендлеры и ресурсы, с ними связанные, освобождаются. При установленной сессии существует ряд функций по управлению транзакциями внутри этой сессии, среди которых TransStart, TransCommit, TransRollback.
Дл работы To8Session необходимо указать объект To8Env. Для этого существует свойство Environment. Строка соединения с сервером определяется свойствами User, Password и Server. После этого устанавливается свойство Active в TRUE. Если Вам удалось установить Active в TRUE, то все в полном порядке и можно работать дальше.
При деактивации сессии просто переведите Active в FALSE. При этом автоматически будут переведены в неактивное состояние все наследники от To8Stmt, связанные с этой сессией. Освободятся хендлеры и ресурсы, связанные с ними.
На один объект To8Env Вы можете повесить несколько объектов To8Session. Тем самым Вы откроете несколько сессий на сервере.
Подробнее хотелось бы поговорить про свойство NonBlocking. Это свойство логического типа, которое устанавливает режим отработки OCI приложением функции OCIStmtExecute. В обычном режиме (блокирующем) при выполнении SQL/PL-SQL посредством вызова функции OCIStmtExecute OCI приложение блокируется и пребывает в состоянии ожидания до тех пор, пока сервер не закончит обработку сделанного запроса. В не блокирующем режиме OCIStmtExecute лишь сообщает серверу, что надо сделать, и управление практически сразу передается OCI приложению. OCIStmtExecute в этом случае возвращает константу OCI_STILL_EXECUTING. При многократном вызове OCIStmtExecute функция будет возвращать константу OCI_STILL_EXECUTING до тех пор, пока сервер не закончит обработку запроса. В процессе выполнения сервером длительной работы в не блокирующем режиме Ваше OCI приложение может прервать выполнение, вызвав OCIBreak. Этот вызов делается из метода To8Session BreakNonBlockingExecution. Схематично вышесказанное можно представить следующем образом:
При помощи свойства NonBlocking компонента To8Session Вы можете менять режим выполнения в исполняющих SQL/PL-SQL компонентах, которые привязаны к данной сессии . Это бывает иногда весьма полезно, особенно при выполнении сервером какой-то непомерно долгой работы. В цикл выборки OCI_STILL_EXECUTING можно вделать выполнение другого SQL запроса, который будет читать откуда-то опубликованное первым процессом свойство для нотификации пользователя о проведенной сервером работе. Только имейте в виду, что выполнение второго запроса должно происходить в другой сессии, иначе ORACLE выдаст ошибку о невозможности выполнить запрос в момент выполнения предыдущего.
Environment: To8Env - Указывает на объект Environment.
User, Password, Server: String - Определяют строку соединения с сервером.
NonBlocking: boolean - Режим работы выполнения SQL/PL-SQL: блокирующий/не блокирующий.
Active: boolean - определяет активность компонента.
Компоненты поддерживают только обычные транзакции к одному серверу. Компоненты не поддерживают распределенных транзакций.
Нигде в коде сервис контекст хендлер не ассоциируется с Transaction хендлером. Это сильно упрощает компоненты, делая их менее уязвимыми для ошибок. Однако я пока не очень чувствую, что функциональная часть от этого сильно пострадала.
В выше описанном случае управление транзакциями целиком и полностью перекладывается на прикладного программиста. Нигде внутри программы мой компонент не начинает никакую транзакцию и тем более сам не блокирует данные. Прикладной программист в данной ситуации, по моему глубокому разумению, должен быть полным хозяином положения.
procedure TransStart - Стартует новую транзакцию.
procedure TransCommit - Без комментариев.
procedure TransRollback - Без комментариев.
Экспорт хендлеров - это просто публикация хендлеров в секции PUBLIC в виде свойств только на чтение для дальнейшего использования другими компонентами. Вот их список:
property hEnv: pOCIEnv read GetEnvhp;
property hServ: pOCIServer read GetSrvhp;
property hSess: pOCISession read usrhp;
property hSvc: pOCISvcCtx read svchp;
property hErr: pOCIError read errhp;
To8Stmt - это базовый абстрактный класс, который служит для одной-единственной цели. Он задает стандартный набор абстрактных методов и свойств, который должен поддерживаться в обязательном порядке (а по-другому и не произойдет, компилятор не пропустит) любым наследником от этого класса, конкретно реализующим абстрактные методы.
Все эти методы и свойства можно сгруппировать следующим образом:
- Методы и свойства, исполняющие SQL/PL-SQL;
- Методы и свойства, обеспечивающие передачу параметров для выполнения и прием результатов в выходные параметры;
- Методы и свойства, управляющие результирующей выборкой (Result Set).
Дл управления параметрами, как входными, так и выходными, определены вспомогательные классы - TOraParams и TOraParam.
Дл выборки результатов из Result Set мной прописан класс TOraField. Поля TOraField работают исключительно с наследниками от To8Stmt, и ни с чем больше. Попрошу, пожалуйста, их не путать со стандартными TField-ами, которые работают с наследниками от TDataSet. С TField-ами Вы тоже можете работать, когда имеете дело с компонентами To8DataSet и To8RODataSet, причем реализован следующий механизм: для To8RODataSet, если Вы меняете значение поля TField, то автоматически меняется значение соответствующего поля TOraField. При изменении в этом случае TOraField значение поля TField меняется только после операции Refresh. Для To8DataSet при изменении полей TField с TOraField ничего не происходит, потому что все изменения происходят во внутреннем буфере To8DataSet.
Методы перемещения по Result Set присутствуют непосредственно в To8Stmt как абстрактные.
Метод непосредственного исполнения SQL/PL-SQL:
- procedure ExecSql; virtual;
Перед вызовом этого метода должны быть определены и присвоены все параметры. Метод НЕ открывает Result Set. Он просто исполняет SQL/PL-SQL.
Существует специальное свойство Exec логического типа. При установке его в TRUE просто происходит вызов метода ExecSql. Если вынести его в секцию PUBLISHED в компоненте-наследнике, то Вы получите удобный инструмент для выполнени SQL/PL-SQL непосредственно из среды разработки без открытия Result Set. Это свойство определено следующим образом:
- property Exec: boolean read FExec write SetExec;
Дл открытия Result Set существуют следующие методы:
- procedure Open; virtual;
- procedure Prepare; virtual;
Дл закрытия:
- procedure Close; virtual;
При открытии Result Set также необходимо наличие всех параметров.
Управление открытием/закрытием Result Set можно возложить на свойство Active:
- property Active: boolean read GetActive write SetActive;
Это свойство выносится в секцию PUBLISHED в классах-потомках и становится доступно через Object Inspector. Поэтому процессом открытия/закрытия можно управлять из среды разработки.
Ничего нового я тут не придумал. Все как обычно: перемещение по выборке может быть абсолютным и относительным. В компонентах-наследниках операция FETCH выполняетс динамически при перемещении по Result Set. Но сами методы Fetch тоже прописаны в классе To8Stmt как абстрактные.
Абсолютное перемещение:
- procedure MoveTo(NewRec: ub4); virtual; abstract;
- procedure MoveToFast(NewRec: ub4); virtual; abstract;
- procedure First; virtual; abstract;
- procedure Last; virtual; abstract;
- property RecNo: ub4 read FRecno write MoveTo;
Относительное перемещение:
- procedure MoveBy(SkipRec: sb4); virtual; abstract;
- procedure Next; virtual; abstract;
- procedure Prior; virtual; abstract;
Метод FETCH:
- procedure FetchAll; virtual; abstract;
Дл определения достижения начала или конца выборки при перемещении имеютс свойства:
- property Eof: boolean read Feof;
- property Bof: boolean read Fbof;
Есть свойство, определяющее, сколько строк было выкачано с сервера операцией FETCH:
- property FetchRowsCount: ub4 read GetFetchRowsCount;
Дл передачи параметров в выполняемый SQL/PL-SQL класс To8Stmt содержит следующие свойства:
- property Params: TOraParams read FParams write SetParamList stored False; - возвращает контейнер параметров.
- property ParamByName[sIndex: String]: TOraParam read GetParamByName; - возвращает класс, описывающий один параметр, по имени этого параметра.
- property ParamByNum[iIndex: Integer]: TOraParam read GetParamByNum; - возвращает класс, описывающий один параметр, по номеру этого параметра.
Прошу отметить очень важный факт. В OCI есть возможность определять параметры двум способами: по именам и по номерам.
Вот пример SQL с именным параметром:
SELECT * FROM EMP WHERE EMPNO = :pEMPNO
Вот пример SQL с номерным параметром:
SELECT * FROM EMP WHERE EMPNO = :1
В компонентах реализован только один способ - по именам. Поэтому в свойстве ParamByNum в качестве индекса используется индекс параметра в контейнере TOraParams, а не номер параметра при номерном способе определени параметров.
- property ParamNum[sIndex: String]: Integer read GetParamNumByName; - возвращает индекс параметра в контейнере параметров TOraParams по его имени.
- property ArrayOfPar: Boolean read FArrayOfPar write SetArrayOfPar; - определяет, как будут интерпретированы параметры при исполнении SQL. Если ArrayOfPar = TRUE, то параметры интерпретируются как массив структур, или как двумерный массив параметров. При исполнении весь массив разом отправляется на сервер вместе с выполняемым SQL/PL-SQL, и там сервер в цикле делает выполнение для каждой строки параметров.
Это очень сильно экономит время, особенно при удаленном доступе к серверу.
- property ArrayParValue[Index1, Index2: Integer]: Variant read GetArrayParValue write SetArrayParValue;
- property ArrayParNull[Index1, Index2: Integer]: Boolean read GetArrayParNull write SetArrayParNull;
Два этих свойства позволяют заполнять двумерный массив параметров при условии, что свойство ArrayOfPar установлено в TRUE.
- property ArrayParRCode[Index1, Index2: Integer]: ub2 read GetArrayParRCode;
Это доступ к выходному массиву кодов возврата для каждого параметра. Код возврата - это код ошибки для каждого параметра. Например, Вы принимаете в выходной параметр строку, и в каком-то месте в массиве параметров строка с сервера не поместилась в отведенное ей место. В этом случае сервер в код возврата по соответствующим индексам массива поместит код ошибки.
- property LastParamRow: ub4 read FLastParamRow write FLastParamRow; - определяет, сколько строк в двумерном массиве параметров. Обычно это свойство использую только на чтение, потому что количество строк подсчитываетс автоматически при использовании свойства ArrayParValue при заполнении этого массива.
Забега вперед, хочу отметить два важных фактора. В компоненте To8StmtOrd имеетс свойство Portion. Это свойство определяет количество строк, которые скачиваютс с сервера за один раз при операции FETCH. Так же при установке свойства ArrayOfPar в TRUE резервируется память для двумерного массива параметров, при этом предполагается, что в этом массиве тоже Portion строк.
Таким образом, во-первых, свойство ArrayOfPar нужно взводить только после установленного свойства Portion, и во-вторых, значение свойства LastParamRow не должно превышать Portion.
По умолчанию Portion равно 100.
Я знаю, что писать компоненты, работа которых сильно зависит от порядка установки свойств - это плохой тон, но так сложилось, извините (это не bug, это feature).
ptString - Строка. Я специально тестировал строковые параметры, как входные, так и выходные, на максимальную длину. В документации прописана цифра 32K. На самом деле чуть чуть поменьше: 32500 байт (на 268 байт меньше чем 32K).
ptInteger - Без комментариев.
ptFloat - Без комментариев.
ptDateTime - Без комментариев.
ptClob, ptBlob, ptCFile, ptBFile - эти параметры лучше брать из TOraField, либо использовать функции EMPTY_CLOB() и EMPTY_BLOB().
ptCursor - при возврате ссылки на курсор или передаче Statement handle.
ptRowId - уникальный идентификатор записи таблицы. Этот параметр как входной тоже надо брать из TOraField, больше неоткуда.
Дл приема результатов запросов класс To8Stmt содержит следующие методы и свойства:
- function RawFieldStructGet(position: sword): pRStruct; virtual; abstract;
- property RawFieldStruct[position: sword]: pRStruct read RawFieldStructGet;
Вытаскивают внутреннюю структуру, описывающую поля полученной выборки.
- function RawFieldGet(position: sword): pSelectListItem; virtual; abstract;
- property RawField[position: sword]: pSelectListItem read RawFieldGet;
Вытаскивают внутреннюю структуру, содержащую ссылки на области памяти, в которую ORACLE поместил значения полей во внутреннем формате, индикаторы, указывающие на NULL значения, на длины значений полей и на коды возврата.
- function FieldByName(sFieldName: String): TOraField;
- function FieldByNum(FieldNum: Integer): TOraField;
- property OraFieldByNum[Index: Integer]: TOraField read GetOraFieldByNum;
- property OraFieldByName[Index: String]: TOraField read GetOraFieldByName;
Выдают экземпляр класса TOraField для поля по имени и по номеру.
- property FieldName[Index: Integer]: String read GetFieldName;
Выдает имя поля по номеру поля.
- property RawBufferGet: PChar read GetRawBuffer;
- property RawIndGet: psb2 read GetRawInd;
- property RawRCodeGet: pub2 read GetRawRCode;
- property RawRlenGet: pub2 read GetRawRlen;
Выдают указатели на запись целиком, на массив индикаторов для записи, на массив кодов возврата и на массив длин.
ptString - Без комментариев.
ptInteger - Без комментариев.
ptFloat - Без комментариев.
ptDateTime - Без комментариев.
LobStream - Дополнительный класс для управления потоком BLOB/CLOB. В классе TOraField есть методы, возвращающие экземпляр TOraLobStream.
ResultSet - Специальный механизм класса TOraStmtOrd позволяет экспортировать открытые Statement хендлеры из Result Set в другой экземпляр класса TOraStmtOrd, и открыть его там.
RowId - Этот хендлер возвращается в Result Set-е, если в запросе Вы определили поле RowID.
Итак, To8StmtOrd.
To8StmtOrd в состоянии исполнять выражения:
На языке определения данных (Data Definition Language - DDL)
На языке манипулирования данными (Data Manipulation Language - DML)
На языке PL/SQL
Анонимные блоки
Процедуры и функции
Пакетные процедуры и функции
Управляющие выражени
Управление транзакциями (COMMIT, ROLBACK)
Запросы (Query)
Это очень просто. Киньте на форму компонент To8Env и активизируйте его. Киньте на форму компонент To8Session, свяжите его с компонентом To8Env при помощи свойства Environment. Установите свойства Server: "<TNS-имя сервиса>", User: "SCOTT", Password: "TIGER". Активизируйте компонент To8Session. Киньте на форму компонент To8StmtOrd. Свяжите его с сессией при помощи свойства oraSession. Напишите SQL выражение в редакторе свойства SQL: "SELECT * FROM EMP". Активизируйте компонент. После всего проделанного у Вас будет открыт Result Set, текущая запись будет первой.
Если в RunTime-е при этом выполнить код:
ShowMessage(o8StmtOrd1.FieldByName('ENAME').AsString);
Вы получите сообщение "KING".
Переведите текущую запись на следующую:
o8StmtOrd.Next;
И опять высветите значение поля ENAME:
ShowMessage(o8StmtOrd1.FieldByName('ENAME').AsString);
Вы получите "BLAKE".
Слегка усложним задачу. Теперь определите в редакторе SQL выражение: "SELECT * FROM EMP WERE EMPNO = :pEMPNO".
А теперь зайдите в редактор свойства Params. В списке параметров Вы увидите свой единственный параметр "pEMPNO". Встаньте на него курсором, и в Object Inspector-е установите свойства этого параметра:
DataType - ptInteger;
ParamValue - 7839;
Выйдите из редактора параметров.
Опять активизируйте To8StmtOrd (после смены SQL выражения свойство Active автоматически приобретает значение FALSE).
И проделайте те же вызовы в RunTime-e. На этот раз Вы получите два раза "KING". Это естественно, потому, что в Result Set всего одна строчка, и сделав Next, Вы уперлись в конец выборки. О чем просигнализирует свойство EOF. Проверьте его, оно должно быть TRUE.
Вот, собственно, в общих чертах, основная схема работы с компонентом. Но это еще далеко не все.
Параметры для To8StmtOrd управляются классом TOraParam. Этот класс обеспечивает резервирование и освобождение памяти под значения параметров, кроме случа двумерного массива параметров, в котором резервированием памяти занимаетс To8StmtOrd. При помощи свойств класса TOraParam Вы получаете возможность устанавливать значения параметров и считывать значения выходных параметров. Также Вы получаете возможность полностью описать характеристики параметра.
Как я уже говорил, связывание параметров происходит исключительно по именам, и ни в коем случае не по номерам. Поэтому везде, где свойство или метод принимает номер параметра, номер следует понимать как индекс параметра в контейнере TOraParams.
Если Вы взглянете на список свойств класса TOraParam, то не найдете там ничего, что описывало бы тип параметра как входного или выходного. Это не случайно. OCI сам понимает, что это за параметр, и в случае, если он OUT - просто помещает в его адрес полученное значение. Компоненту TOraParam его надо только прочитать, что он с успехом и делает.
В SQL/PL-SQL может присутствовать один и тот же параметр несколько раз, то есть адрес, конечно, один, а вот ссылка на этот адрес в SQL/PL-SQL встречаетс несколько раз. Компонент TO8StmtOrd прекрасно справляется с этой ситуацией, чего не скажешь о, например, BDE.
Не будет новостью, если я скажу, что параметр может быть NULL значением. В классе TOraParam для этого случая есть свойство IsNull, логического типа.
Кроме того, каждый параметр имеет код возврата.
В роли значений может выступать двумерный массив параметров. Этим управляет сам компонент To8StmtOrd посредством свойства ArrayOfPar.
Открыты для среды разработки следующие свойства:
property Name: String read FParName write FParName; - имя параметра. Должно совпадать с именем в SQL/PL-SQL. При редактировании свойства SQL компонента To8StmtOrd происходит парсинг SQL выражения, и параметры, встреченные там, определяются сами. Свойство Name заполняется автоматически. Из RunTime-а параметры также заполняются автоматически при изменении SQL.
property DataType: TRawParamType read FDataType write SetDataType; - тип параметра. В настоящее время доступны типы: ptString, ptInteger, ptFloat, ptDateTime, ptCLob, ptBLob, ptCFile, ptBFile, ptCursor, ptRowID.
property MaxLengthStringBuffer: Integer read FMaxLengthStringBuffer write SetMaxLengthStringBuffer; - определяет максимальную длину строки в байтах при типе параметра ptString плюс один байт на конечный ноль. По умолчанию 256. Максимальное значение для ORACLE - 32500 байт. Рекомендуется делать на 3 - 5 байт больше, чем максимальная длина Вашей строки.
property IsNull: boolean read GetIsNull write SetIsNull; - определяет NULL значение для параметра.
property ParamValue: Variant read GetValue write SetValue; - устанавливает значение параметра и читает значение выходного параметра. Кроме этого универсального свойства, существуют узкоспециализированные PUBLIC свойства типа AsString, AsInteger, AsFloat, AsDateTime. Есть свойство LobStream, которое возвращает экземпляр класса TOraLobStream, если это LOB параметр.
Дл параметров Cursor и RowId можно получить соответствующие хендлеры благодар PUBLIC свойствам:
property Cursor: pOCIResult read BufCursor;
property RowId: pOCIRowid read GetBufRowId write SetBufRowId;
Это очень мощный инструмент и он не прописан для BDE, отчего приобретает еще большую привлекательность. Суть его состоит в следующем. OCI отправляет на сервер SQL/PL-SQL предложение и вместе с ним весь двумерный массив параметров (как написано в документации, за один Round trip, т.е. за один раз). Сервер выполняет SQL/PL-SQL (это может быть один оператор SQL или анонимный PL-SQL блок кода, или вызов какой-нибудь хранимой процедуры, и т.д.) заданное количество раз, начиная с определенного индекса строчкам параметров. Это очень сильно повышает производительность и уменьшает сетевой трафик.
Дл указания того, что Вы будете работать с параметрами - двумерными массивами, Вы устанавливаете логическое свойство ArrayOfPar в значение TRUE. Именно при этой установке компонент To8StmtOrd резервирует память для параметров. По этой причине на момент установки ArrayOfPar в TRUE необходимо определить типы всех параметров и максимальное количество строк в массиве параметров. В среде разработки и в момент загрузки формы за порядком установки свойств следит метод Loaded, т.е. это происходит автоматически. Если Вы пишете ручную обработку, то Вам надо следить за этим самому. Максимальное количество строк в массиве параметров определяется свойством Portion (это свойство также используется дл определения количества строк для операции Fetch). Поэтому значение этого свойства должно быть определено заранее. (Я уже говорил, что это не здорово, но так сложилось.) По умолчанию оно равно 100.
Дл установки значений и индикаторов служат свойства:
property ArrayParValue[Index1, Index2: Integer]: Variant read GetArrayParValue write SetArrayParValue; - устанавливает значения переменных. Index1 - индекс строки в массиве параметров. Index2 - индекс параметра в контейнере TOraParams.
property ArrayParNull[Index1, Index2: Integer]: Boolean read GetArrayParNull write SetArrayParNull; - устанавливает NULL значения для параметров. Индексы те же самые.
property ArrayParRCode[Index1, Index2: Integer]: ub2 read GetArrayParRCode; - служит для извлечения кода возврата для каждого значения выходных параметров в массиве параметров. Индексы те же самые.
property LastParamRow: ub4 read FLastParamRow write FLastParamRow; - определяет конечную строку в массиве параметров. Не должно быть больше чем свойство Portion. Заполняется автоматически при заполнении свойства ArrayParValue.
Пример.
Дл предыдущего примера переделайте свойство SQL компонента To8StmtOrd:
INSERT INTO DEPT (
DEPTNO,
DNAME,
LOC
) VALUES (
:pDEPTN,
:pDNAME,
:pLOC
)
Теперь зайдите в редактор свойства Params. В списке параметров Вы увидите параметры "pDEPTN", "pDNAME", "pLOC". Определите свойства для параметров.
"pDEPTN":
DataType - ptInteger;
MaxLengthStringBuffer - 5; (чтобы зря память не расходовать)
"pDNAME":
DataType - ptString;
MaxLengthStringBuffer - 20; (максимальная длина строки + 5 на всякий случай)
"pLOC":
DataType - ptString;
MaxLengthStringBuffer - 20; (максимальная длина строки + 5 на всякий случай)
Выйдите из редактора параметров.
Установите свойство компонента To8StmtOrd ArrayOfPar в TRUE.
Повесьте на форму кнопку и напишите код для обработчика нажатия:
o8StmtOrd1.ArrayParValue[0, 0] := 50;
o8StmtOrd1.ArrayParValue[0, 1] := 'q1';
o8StmtOrd1.ArrayParValue[0, 2] := 'w1';
o8StmtOrd1.ArrayParValue[1, 0] := 60;
o8StmtOrd1.ArrayParValue[1, 1] := 'q2';
o8StmtOrd1.ArrayParValue[1, 2] := 'w2';
o8StmtOrd1.ArrayParValue[2, 0] := 70;
o8StmtOrd1.ArrayParValue[2, 1] := 'q3';
o8StmtOrd1.ArrayParNull[2, 1] := true;
o8StmtOrd1.ArrayParValue[2, 2] := 'w3';
o8Session1.TransStart;
try
o8StmtOrd1.ExecSql;
o8Session1.TransCommit;
except
o8Session1.TransRollback;
end;
После того как вы запустите приложение и нажмете кнопку, три строки будут вставлены в таблицу DEPT. Вы это можете проверить в SQL*Plus.
LOB-ы - это очень большой шаг вперед по сравнению с LONG. Я не буду пока вдаваться в подробности, но мой Вам большой совет: переделайте все Ваши Long-и на Blob-ы и Clob-ы.
LOB-ы в ORACLE бывают внутренние и внешние. Различаются они тем, что внутренние лежат непосредственно в базе данных и подвержены всем прелестям транзакционного механизма, т.е. могут быть модифицированы в любой момент. Внешние лежат в файловой системе той операционной системы, под которой работает сервер ORACLE, и не могут быть модифицированы. Они работают только на чтение.
Если говорить очень упрощенно, то работа CALL-интерфейса с LOB заключается в получении LOB-хендлера, который можно получить из Result Set или из выходного параметра привязки. После чего в OCI существует целая гора функций, которые делают с этим LOB-ом через его хендлер все что угодно. Скачут в любое место этого LOB-а, считывают и записывают в него информацию, и т.д. Работа с LOB-хендлером очень напоминает работу с обычным файловым хендлером.
LOB-хендлер в моем компоненте помещается во внутреннюю переменную, определенную классом TOraParam. Далее мной прописан специальный класс, наследник от стандартного VCL класса TStream - TOraLobStream со стандартными методами Read, Write и Seek. И уже благодаря этому высокоуровневому классу с LOB-ом можно делать все что угодно.
Дл того, чтобы получить экземпляр TOraLobStream из TOraParam, в TOraParam есть свойство:
property LobStream: TOraLobStream read GetLobStream;
Кроме того, Вы можете непосредственно добраться до самого LOB-хендлера:
property LobLob: pOCIBlobLocator read BufLob; - до внутреннего LOB-а.
property LobFile: pOCIBlobLocator read BufFile; - до внешнего.
Хот этого делать я Вам не рекомендую.
Пример работы с LOB-ами.
Дл примера нам потребуется таблица, содержащая LOB значения. Получить ее можно по-разному, но я предлагаю воспользоваться моей утилитой NLOPlus. Эта утилита, естественно, написана с использованием компонент NLO. Загрузите NLOPlus. Перед загрузкой она спросит у Вас LogOn информацию: User, Password и Server. По пункту меню Work->New SQL Editor Вы получите на экране редактор для редактировани SQL/PL-SQL и грид для показа Result Set.
В редакторе наберите текст:
create
table blobtest (
f1 integer,
f2 blob,
f3 clob,
f4 bfile)
И нажмите единственную кнопку с молнией. Таблица создастся на сервере. При написании статьи я использую сервер, который крутится у меня на машине, поэтому я точно знаю все свои директории и имею к ним доступ. Я также являюсь для своего сервера системным администратором, поэтому я могу организовать у себя доступ к внешнему LOB-у. Если у Вас нет такой возможности, то опустите поле f4 при создании таблицы. Итак, для доступа к внешнему LOB-у я должен определить на сервере объект, который называется DIRECTORY. Это всего лишь псевдоним физической директории в файловой системе, в которой вертится сервер. Список всех объектов DIRECTORY можно получить при помощи VIEW ALL_DIRECTORIES. Создать новую директорию можно при помощи DDL команды CREATE DIRECTORY.
Я создаю такую и мапирую ее на свою внутреннюю директорию:
CREATE DIRECTORY VLAD AS 'E:\NLO\BIN'
Дл создания директории у Вас должны быть привилегии для ее создания.
В директории E:\NLO\BIN у меня лежит файл NLO.ZIP - архив с исходными кодами компонентов.
Добавим строку в таблицу blobtest:
insert into blobtest (
f1,
f2,
f3,
f4
) values (
10,
empty_blob(),
empty_clob(),
bfilename('VLAD', 'NLO.ZIP')
)
Обратите внимание на использование функций SQL. Empty_Blob() и Empty_Clob() возвращают пустой BLOB и CLOB соответственно. Функция BFILENAME принимает в качестве параметров имя директории как строку и имя файла в этой директории. Им директории должно быть БОЛЬШИМИ БУКВАМИ. В свое время я много потратил усилий, чтобы это выяснить. BFILENAME возвращает уже проинициализированный дескриптор BFILE. Таким образом, вышеприведенный оператор INSERT добавляет в таблицу строку с двумя пустыми LOB-ами и с BFILE, указывающим на E:\NLO\BIN\NLO.ZIP.
Дл заполнения двух внутренних LOB-ов воспользуемся все той же NLOPlus.
Наберите в SQL редакторе:
select * from blobtest for update
Предложение FOR UPDATE блокирует выбранные строки для последующего изменения. В противном случае OCI позволит только читать LOB-ы и не позволит их изменять.
После выполнения (кнопка с молнией) грид будет содержать одну строчку Result Set:
Дл того, что бы в LOB закачать файл, необходимо два раза щелкнуть мышью по полю в гриде. Давайте начнем с поля F2 - BLOB. Щелкните два раза мышью на поле F2, вы получите окно просмотра BMP файлов. Если бы в BLOB в поле F2 находилась картинка BMP, то это окно показало бы ее. Но поле F2 пусто, поэтому и окно пустое. Нажмите единственную кнопку в Tool bar-е, Вы получите диалоговое окно OPEN FILE, в котором можете выбирать только BMP файлы. Выберите один из них со своего диска. Компонент закачает BMP файл в BLOB и закроет окно. При повторном двойном щелчке мыши окошко высветит ту же картинку, которую вы туда закачали.
Повторите проделанное с полем F3, с учетом того, что это окошко понимает только файлы в формате RTF.
Наберите в редакторе SQL COMMIT и выполните. Этим Вы завершите транзакцию и освободите строки, выбранные SELECT ... FOR UPDATE.
Теперь можно приступать к коду приложения примера.
Воспользуемс ранее приведенным примером. В компоненте To8StmtOrd измените SQL выражение:
begin
select f2, f3 into :pf2, :pf3 from blobtest where f1 = 10;
:pf4 := BFILENAME('VLAD', 'NLO.ZIP');
end;
или
begin
select f2, f3, f4 into :pf2, :pf3, :pf4 from blobtest where f1 = 10;
end;
Попробуйте и так, и так. Определите три выходные переменные как BLOB, CLOB и BFILE. На кнопку повесьте код:
procedure TForm3.Button1Click(Sender: TObject);
var
sF2, sF3, sF4: TFileStream;
l2, l3, l4: TOraLobStream;
begin
sF2 := TFileStream.Create('Blob.bmp', fmCreate);
sF3 := TFileStream.Create('Clob.rtf', fmCreate);
sF4 := TFileStream.Create('BFile.zip', fmCreate);
o8StmtOrd1.ExecSql;
l2 := o8StmtOrd1.ParamByName['pf2'].LobStream;
l3 := o8StmtOrd1.ParamByName['pf3'].LobStream;
l4 := o8StmtOrd1.ParamByName['pf4'].LobStream;
sF2.CopyFrom(l2, l2.Size);
sF3.CopyFrom(l3, l3.Size);
sF4.CopyFrom(l4, l4.Size);
beep;
end;
После выполнения процедуры все три LOB-а должны перекачаться в файлы: 'Blob.bmp', 'Clob.rtf', 'BFile.zip'.
Попробуйте, Вам понравится.
Надеюсь, Вы в курсе того, что PL-SQL блок кода или процедура может возвращать сколь угодно много открытых курсоров (переменных типа REF CURSOR). С такими переменными могут работать многие продукты, в том числе и BDE. Но меня всегда убивало отсутствие возможности в BDE обработать сразу несколько возвращаемых процедурой открытых курсоров. Другое дело - компонент To8StmtOrd. Если в качестве параметра встречается переменная с типом REF CURSOR (ptCursor), то компонент в параметре просто возвращает открытый Statement хендлер. Другой экземпляр этого же компонента может взять этот готовый Statement хендлер и открыть по нему Result Set. И так столько раз, сколько переменных REF CURSOR. Не правда ли, здорово? Только надо пояснить, что значит "может взять этот готовый Statement хендлер".
To8StmtOrd содержит свойства:
property CursorParam: TOraParam read FCursorParam; - указывается конкретно объект TOraParam с типом ptCursor.
Либо:
property CursorSource: To8Stmt read FCursorSource write SetCursorSource; - указывается To8Stmt класс, содержащий параметры курсоры.
property CursorIndex: Integer read FCursorIndex write SetCursorIndex; - указываетс индекс параметра курсора в списке параметров TOraParams.
Последние два свойства видны в Object Inspector. CursorParam доступен для ручного присваивания.
To8StmtOrd спроектирован таким образом, что если указаны эти свойства, то выражение SQL просто игнорируется и открывается Result Set по готовому Statement хендлеру, который берется из указанного параметра. Как видите, существует два способа указать этот параметр.
Разберем пример. Опять чуть модифицируем старый. Изменим свойство SQL:
DECLARE
TYPE TQ1 IS REF CURSOR;
TYPE TQ2 IS REF CURSOR;
Q1 TQ1;
Q2 TQ2;
BEGIN
OPEN Q1 FOR SELECT * FROM EMP;
OPEN Q2 FOR SELECT * FROM DEPT;
:pQ1 := Q1;
:pQ2 := Q2;
END;
В параметрах укажем, что параметры имеют тип ptCursor. Это у нас не запрос, поэтому открывать нечего, но его надо выполнить. Из среды разработки это можно сделать, установив свойство Exec в TRUE, метод по записи которого выполняет ExecSql.
Собственно, все. Открытые Statement хендлеры уже присутствуют в объектах параметрах. Все остальные действия будут направлены на то, чтобы высветить полученные курсоры.
Шлепнем на форму еще два объекта To8StmtOrd и свяжем их с той же сессией. Обоим объектам укажем в качестве источника курсора объект o8StmtOrd1, установив свойство CursorSource. CursorIndex у одного укажем 0, у другого 1. И переведем оба объекта в активное состояние. На данный момент оба Result Set открыты.Чтобы показать их в Design time, забежим вперед и воспользуемс компонентом To8RODataSet. Возьмите его с палитры и два раза поместите на форму. Свяжите один с o8StmtOrd2, второй с o8StmtOrd3, и переведите в активное состояние. Дальше стандартным образом: к обоим привяжите DataSource, к DataSource - DBGrid-ы. В DBGrid-ах Вы тут же будете лицезреть таблицы Emp и Dept, открытые компонентом o8StmtOrd1:
Нравится? Дальше - лучше.
RowId - это уникальные идентификаторы записей в таблице ORACLE. Что они из себ представляют изнутри, мне лично совсем неинтересно. С точки зрения OCI, это хендлеры со всеми вытекающими последствиями. Естественным образом было бы их использовать в качестве параметров для наибыстрейшего получения полей строки таблицы. Для этого в TOraParam предусмотрено свойство:
property RowId: pOCIRowid read GetBufRowId write SetBufRowId;
При помощи этого свойства и устанавливаются и выбираются конкретные значения RowId.
Не мудрствуя лукаво, сразу пример.
Удалите все, что связано с o8StmtOrd2 и o8StmtOrd3. Установите свойство SQL o8StmtOrd1:
select
emp.*,
dept.dname,
dept.RowID
from
emp,
dept
where
emp.deptno = dept.deptno
Обратите внимание на то, что выбирается из таблицы DEPT: DNAME для проверки и RowId.
Активизируйте o8StmtOrd1. Уроните на форму To8RODataSet, свяжите его с o8StmtOrd1 и тоже активизируйте. Сделайте двойной щелчок мыши на компоненте o8RODataSet1 - вы попадете в редактор полей. Добавьте все имеющиеся поля и одно Calculated, я его назвал "QQ" и тип ему сделал такой же, как и у dname - String. Далее стандартным образом: DataSource, DBGrid. В DBGrid-е при этом должны появиться живые данные.
Calculated-пол вычисляются в событии OnCalcFields. Чтобы вычислить значение поля QQ (а его хочу сделать таким же, как значение DNAME, только вытащить через RowId), должен поместить на форму еще один компонент o8StmtOrd2, связать его с сессией и определить для него SQL:
begin
select dname into :pdname from dept where rowid = :prowid;
end;
Далее определяю для него два параметра - pdname и prowid.
Событие OnCalcFields компонента To8RODataSet выглядит у меня следующим образом:
procedure TForm5.o8RODataSet1CalcFields(DataSet: TDataSet);
begin
o8StmtOrd2.ParamByNum[0].AsString := '';
o8StmtOrd2.ParamByNum[1].RowId :=
o8RODataSet1.GetAsRowID(o8RODataSet1.FieldByName('ROWID'));
o8StmtOrd2.ExecSql;
o8RODataSet1qq.Value := o8StmtOrd2.Params[0].AsString;
end;
Как видите, для того, что бы вытащить dname по RowId, RowId берется непосредственно из DataSet выражением o8RODataSet1.GetAsRowID(o8RODataSet1.FieldByName('ROWID')), и заполняетс значение параметра. Выполняется SQL. И после этого заполняется значение поля QQ.
Вот что должно получится в итоге:
Посмотрите, значения полей QQ и DNAME полностью совпадают.
Выполнение запросов накладывает на компонент To8StmtOrd дополнительные требования, связанные с тем, что запрос не только исполняет SQL оператор SELECT, но должен еще получить результирующую выборку, образующуюся в результате запроса, и уметь с ней работать. Под работой с выборкой я подразумеваю хранение выборки на клиенте, навигацию по этой выборке, выкачивание ее с сервера на клиента и, наконец, получение значений полей в удобном для пользователя виде. Все вместе это представляет из себя отнюдь не тривиальную задачу.
Однако To8StmtOrd прекрасно с ней справляется.
Выкачиванием, навигацией и хранением занимается непосредственно компонент To8StmtOrd. Работа с полями выборки осуществляется вспомогательным классом TOraField.
Операци Fetch происходит всегда в один и тот же буфер памяти, который организован в виде массива структур. Структура одной строки массива отражает структуру записи Result Set. Вышесказанное не значит, что буфер предназначен исключительно дл операции Fetch. Это просто текущий динамический буфер. Его можно рассматривать как скользящее по выборке окно. Когда Вы перемещаетесь по выборке (Next, MoveTo, MoveBy, Prior, First, Last), Вы перемещаете указатель внутри буфера. Когда указатель достиг нижней или верхней границы буфера, происходит смена буфера, причем текущий буфер-окно либо считывается из хранилища Result Set, либо непосредственно в него делается очередная операция Fetch. Таким образом, данные выборки подкачиваются с сервера динамически при перемещении по ней и только в том случае, когда они нужны. Количество строк буфера-окна определяется свойством To8StmtOrd Portion, по умолчанию его значение равно 100. Естественно, что это свойство влияет на быстродействие компонента, потому что от него зависят операции чтения/записи из хранилища Result Set и операции выкачивания данных с сервера (FETCH). Но не стоит сильно перегибать при установке этого свойства: память не резиновая и возможности сети не безграничны.
Помимо динамической операции Fetch, которая не доступна никому, кроме самого компонента, существует операция FetchAll, доступная всем. О ее предназначении говорит ее название: выкачать все. Так оно и происходит.
Неприятно, конечно, констатировать, но библиотека OCI не лишена ошибок. Одна из этих ошибок накладывает отпечаток на операцию FETCH для To8StmtOrd. Дело в том, что, как я сказал, буфер для FETCH организован в виде массива структур. Это значит, что я должен использовать механизм Array of structures применительно к результирующей выборке. И все бы было здорово, но если в выборке есть дескрипторы (Lob, RowId, Statement handler), то механизм Array of structures дл Define переменных не работает. Конкретно функция OCIDefineArrayOfStruct устанавливает skip параметры, которые не отрабатываются при наличии в выборке дескрипторов при дальнейшей операции FETCH, что приводит к краху программы.
Выкрутилс я из данной ситуации очень просто. Я принудительно, при наличии в выборке хендлеров (Lob, RowId, Statment), устанавливаю свойство Portion внутри компонента в 1. Этим я не только фактически отключаю механизм Array of structures для Define переменных, но делаю размер буфера-окна равным одной записи (что неблагоприятно сказывается на быстродействии).
Есть информация, что в OCI версии 816 эта ошибка устранена. Я это еще не проверил, потому что моментально на этой версии нашел другую, куда более для мен серьезную. У меня перестал работать механизм Array of structures для Bind параметров, конкретно функция OCIBindArrayOfStruct, т.е. напрочь отключилось то, что я описывал в разделе Параметры массивы. После этого вышло уже 2 Patch-а, но их я еще не посмотрел.
Когда устоится версия 816, я, безусловно, все это проверю, но осадок уже весьма неприятный.
Эта ошибка преследовала при написании своих компонентов и других разработчиков, в частности, Дмитрия Арефьева (dmitrya@inthink.com). Но он подошел к вопросу более кардинально. Он изменил Layout данных для Fetch. Сделал из них не массив структур, а просто линейные массивы со skip параметром, равным 0. В этом случае вызовы OCIDefineArrayOfStruct и OCIBindArrayOfStruct вовсе не требуются и проблема отпадает.
Он меня уже почти убедил в том, что работать нужно именно таким образом, и я уже почти принял решение написать компонент, подобный To8StmtOrd, но с Fetch буфером в виде линейных массивов для каждого поля. Только когда это произойдет, я не знаю, и обещать ничего не могу.
В общем, это в планах, а пока не удивляйтесь, почему свойство Portion вдруг стало равно 1 при наличии BLOB в выходной выборке.
Дл выборок, не содержащих хендлеров, все работает отлично и скорострельность как у авиационного пулемета.
В предыдущем разделе я упомянул термин "хранилище Result Set". Что это такое? Это место, где физически лежит выкачанный с сервера Result Set. Это хранение можно организовать очень по-разному, во всяких хитрых и не очень структурах данных.
Дл To8StmtOrd справедливо следующее:
Result Set хранится в ЛИНЕЙНОЙ структуре.
Физически эта линейная структура может лежать либо в памяти (на самом деле в TMemoryStream, а это еще не значит, что в оперативной памяти), либо в файле на диске.
Линейна организация хранилища данных Result Set дает большие преимущества в скорости работы с такой структурой, но большие проблемы при редактировании. Я имею в виду, что операции Insert и Delete на линейной структуре делаются с трудом, изменение же полей трудностей не вызывает.
По этой причине операций Insert и Delete в компоненте To8StmtOrd нет вовсе. Не ищите их в списках методов. А вот поля TOraField работают как на чтение, так и на запись. Только имейте в виду, что эти изменения отражаются ИСКЛЮЧИТЕЛЬНО в хранилище Result Set на клиенте, и ни в коем случае не на сервере. Для изменени данных на сервере Вам необходимо проделать одну или несколько транзакций с UPDATE.
Пусть Вас не печалит отсутствие возможности Insert и Delete в To8StmtOrd. Они принципиально не нужны в этом компоненте. Его изначальное предназначение - просто получить выборку в максимально короткий срок и как можно быстрей по ней перемещаться, а редактирование - не его прямая задача. Для этого существует специальный компонент To8DataSet со всем необходимым для редактирования и дл отражения изменений на сервере. (Я имею в виду механизм Cached updates.)
Результирующую выборку Вы можете отправить как в память, так и в файл на диске. Все, что нужно сделать - это установить свойство
property ResultSetStream: TResultSetStream read GetResultSetStream write SetResultSetStream;
в значение либо rsToFile, либо rsToMemory.
Как показала практика, хранилище в файле работает быстрее, чем хранилище в памяти, особенно на больших выборках.
При хранении Result Set в файле в системной директории TMP создаются четыре временных файла, содержащие Result Set, индикаторы, коды возврата и длины полей.
Компонент корректно после себя чистит TMP директорию. Но при обслуживании программ возьмите за правило туда временами заглядывать. Разные могут быть ситуации.
Эти поля работают исключительно с наследниками от To8Stmt. Их работа основана на абстрактных методах класса To8Stmt, и они не имеют никакого отношения к TField.
С точки зрения прикладного программиста, это просто удобный механизм выборки данных из Result Set, выкачанного наследниками класса To8Stmt.
Эти поля имеют методы работы со значениями как по чтению, так и по записи вследствие причин, указанных в предыдущем разделе, но работают только с локальной копией Result Set.
В сущности, это единственный инструмент для выборки и установки значений полей дл To8Stmt.
Как и параметры, TOraField могут быть NULL значениями и имеют коды возврата.
property FieldName: String read FFieldName; - имя поля.
property OraFieldType: Integer read GetOraFieldType; - тип поля в терминах OCI констант SQLT_*.
property Value: Variant read GetAsVariant write SetAsVariant; - значение поля. Это универсальное свойство. Существуют более конкретные свойства типа AsString, AsInteger, AsDateTime и т.д.
property IsNull: boolean read GetIsNull write SetIsNull; - определяет NULL значение для поля.
property LobStream: TOraLobStream read GetLobStream; - выдает объект TOraLobStream, если поле LOB.
property ResultSet: To8Stmt read GetResultSet; - выдает экземпляр класса To8StmtOrd в случае, если поле Result Set.
property Cursor: pOCIResult read GetCursor; - выдает Statement хендлер в случае если поле Result Set.
property RowId: pOCIRowid read GetRowId; - выдает указатель на RowId для дальнейшего использования в качестве параметров, например.
Вот и все. Ничего экстраординарного.
Работа с полями LOB в компонентах NLO ничем не отличается от работы с ними, например, в BDE. (Хотя надо отдать должное тому, что BDE вплоть до 5 версии Delphi с ORACLE LOB вообще никак не работало - не умело. А после пятой версии стало работать по той же технологии, как работало с LONG, т.е. через одно всем известное место.)
Схему работы Вы вкратце уже знаете.
Вначале делается SELECT (LOB Fields list) FROM LobTable FOR UPDATE. FOR UPDATE - это важно, так как блокирует данные SELECT-а.
Получив в LOB Fields list дескрипторы LOB-ов, получаем объекты TOraLobStream через пол выборки (свойство LobStream). Работа с LOB идет посредством основных методов этого класса: Seek, Read, Write, Size. После модификации LOB значений делаетс COMMIT.
Весьма интересное поле деятельности вырисовывается при стыковке LOB со стандартными компонентами, рассчитанными на их показ и редактирование, такими как DBImage и DBRichEdit. Для того, чтобы заработали эти компоненты, необходимо прописать виртуальный метод CreateBlobStream для компонентов, наследников от TDataSet: To8RODataSet и To8DataSet. Не буду вдаваться в подробности, как он написан, но он мной написан для обоих компонентов. В результате чего можно построить следующий пример.
Пример.
Будем продолжать корежить наше единственное приложение.
Оставьте на форме только To8Env, To8Session, To8StmtOrd. В компоненте To8StmtOrd поменяйте SQL:
SELECT * FROM BLOBTEST
Активизируйте компонент. Положите на форму, ну, например, To8RODataSet, свяжите его с To8StmtOrd и тоже активизируйте.
Положите на форму TDBImage и TDBRichEdit и TDataSource. TDataSource свяжите с To8RODataSet и приделайте к нему TDBImage и TDBRichEdit к полям соответственно F2 и F3.
В этих двух DB-aware контролах Вы немедленно увидите, уже в Design time-е, те файлы, которые Вы туда положили, разбирая пример в разделе Параметры LOB.
Вот что получилось в моем случае:
В левой части DBImage с картинкой, которую Вы уже видели ранее. В правой DBRichEdit с моей статьей по OCI, написанной в RTF формате в 1998 году дл конференции "Техникон-98". Размер этого RTF файла пол-мегабайта.
Пол курсоры получаются при выполнении запросов следующего плана:
select
emp.*,
cursor(select * from dept where deptno >= emp.deptno) c1
from
emp
В этой команде SELECT определено поле курсор C1, которое представляет из себ коррелированный запрос из таблицы DEPT.
Приятно отметить, что BDE и стандартные компоненты, работающие через BDE, вообще не понимают такого SELECT-а.
При помощи компонента To8StmtOrd запросы подобного вида можно обрабатывать двум способами. Первый заключается в установке свойства
property ResultSet: pOCIResult read FResultSet write FResultSet;
Второй - в установке свойств
property DataSetSource: To8Stmt read FDataSetSource write SetDataSetSource;
property DataSetFieldName: String read GetDataSetFieldName write SetDataSetFieldName;
Последние два свойства видны в Object Inspector, поэтому этим можно заниматься в Design time-е.
Пример.
Оставьте на форме только To8Env, To8Session, To8StmtOrd. Установите для To8StmtOrd SQL в SELECT, указанный ранее. Активизируйте компонент. Добавьте To8RODataSet, TDataSource и TDBGrid. В TDBGrid-е Вы увидите таблицу EMP и поле C1, значениями которого будут DataSet-ы.
Поместите на форму еще один To8StmtOrd и свяжите его с сессией. У компонента To8RODataSet переопределите событие AfterScroll. На это событие я хочу повесить код, при помощи которого буду открывать курсоры C1, вот он:
procedure TForm7.o8RODataSet1AfterScroll(DataSet: TDataSet);
begin
o8StmtOrd2.Active := false;
o8StmtOrd2.ResultSet := o8RODataSet1.GetAsResultSet(o8RODataSet1.FieldByName('C1'));
o8StmtOrd2.Active := true;
o8RODataSet2.Active := true;
end;
Дл показа поля C1 поместите на форму еще To8RODataSet, TDataSource и TDBGrid и привяжите их к o8StmtOrd2.
Запустите приложение. У Вас должно получиться что-то типа вот этого:
Если Вы будете двигаться по верхнему гриду курсором вниз, то у Вас будет открыватьс Result Set из поля C1. Результат будет немедленно отражаться в нижнем гриде. Если Вы попробуете двинуться курсором вверх, то нижний грид очистится. Это объясняется тем, что если Result Set, принадлежащий Statement хендлеру, один раз весь выкачать операцией FETCH, то Statement хендлер автоматически закрывается, и повторно с ним работать уже невозможно. Это особенность полей курсоров, поэтому при построении приложений имейте это в виду.
Второй пример демонстрирует, как то же самое можно сделать при помощи свойств DataSetSource и DataSetFieldName.
Еще раз выкиньте все с формы, кроме трех компонентов To8Env, To8Session, To8StmtOrd. В To8StmtOrd должен остаться SELECT, приведенный в начале раздела. Поместите на форму цепочку компонентов от To8StmtOrd до TDBGrid. Во втором экземпляре To8StmtOrd установите свойства DataSetSource и DataSetFieldName в значения o8StmtOrd1 и C1. Поместите на форму кнопку и напишите следующую реакцию на нажатие:
procedure TForm8.Button1Click(Sender: TObject);
begin
o8StmtOrd2.Active := false;
o8StmtOrd1.Next;
o8StmtOrd2.Active := true;
o8RODataSet2.Active := true;
end;
Как видите, при каждом нажатии кнопки происходит перемещение o8StmtOrd1 и переоткрытие o8StmtOrd2 и o8RODataSet2.
На экране должно быть следующее:
При запуске приложения из среды разработки произойдет преднамеренная ошибка "Нет поля". Так и должно быть, просто продолжите выполнение.
Нажмите несколько раз кнопку. Содержание грида будет меняться.
В заключение скажу, что предпочтительнее использовать первый способ работы с полями курсорами.
Пол LONG остались в наследство от ORACLE 7 и отличаются тем, что хранятс непосредственно в табличном пространстве. Поле типа LONG может быть только одно в таблице, но их может быть несколько в выборке. Основная трудность работы с полями этого типа заключается в том, что их приходится при запросе вытаскивать ВСЕ, сколько бы их ни было и какой длины они бы ни были. При этом при операции FETCH используется специальный механизм, который называется PieceWise operation. Он подразумевает FETCH Result Set по одной строчке и для каждой строчки вытаскивание всех полей типа LONG по кусочкам. При операциях INSERT и UPDATE предполагался тот же механизм, только с точностью до наоборот.
Вы представить себе не можете, как это сложно описать в программе, и еще сложней это отладить. Кроме того, все программное обеспечение, которое работало с LONG, делало это по одной и той же схеме (естественно, потому что другую схему просто нельзя было и придумать). Оно засасывало все LONG-и в память операцией PieceWise. При модификации значений LONG полей, по закрытии курсора, как правило, формировался дополнительный UPDATE или INSERT, который заливал их обратно той же самой операцией. Можете себе представить этот кошмар? Особенно если длина значений LONG полей измеряется сотнями мегабайт.
LOB-ы в ORACLE 8 работают принципиально по-другому. В Result Set приходит лишь дескриптор - ма-а-аленькая структурка, и если Вам потребовалось значение LOB-а, то Вы можете его получить специальными функциями. Это очень похоже на работу с файловым хендлером.
К сожалению, очень многое программное обеспечение, будучи написанное и отлаженное под LONG-и, и с LOB-ом продолжает работать точно так же как с LONG-ом, это и понятно, зачем писать чего-то еще?
BDE с ORACLE LOB работает точно так же, как раньше с LONG-ом. Этим же отличаетс предпоследняя версия Direct Oracle Access. (Про последнюю пока ничего сказать не могу.)
Но с уверенностью могу сказать про VK NLO: с LOB-ами мои компоненты работают оптимальным способом, получая значение LOB-а только в случае необходимости и отправляя на сервер только то, что Вы сами туда отправите.
А вот с LONG VK NLO не очень дружит. И я это сделал совершенно сознательно и намеренно.
LONG значение VK NLO воспринимает как просто длинную строку. Максимальную длину этой строки Вы можете установить сами свойством
property MaxLongSize: ub4 read FMaxLongSize write FMaxLongSize default MAX_LONG_SIZE;
по умолчанию максимальный размер LONG значения может быть до 8K.
Никаких операций PieceWise, боже упаси. Все FETCH-ится за один раз.
Этим маленьким ограничением длины LONG-а на клиенте я ОЧЕНЬ СИЛЬНО упростил себе жизнь, с одной стороны, но, с другой стороны, я все-таки прекрасно могу с ними работать.
Значение LONG-а я могу получить из свойства AsString класса TOraField.
Дл примера выполните любой SELECT, содержащий LONG значения, компонентом To8StmtOrd и попытайтесь высветить значение LONG поля:
ShowMessage(o8StmtOrd1.OraFieldByName['LONG_FIELD'].AsString);
Дл того, чтобы заполнить LONG значение, используйте параметр AsString, установив длину LONG-а свойством MaxLengthStringBuffer.
Я уже описывал принципиальную схему работы не блокирующего режима в разделе Компонент Oracle Session – To8Session, поэтому делать больше этого не буду, а опишу, как он работает, применительно к компоненту To8StmtOrd.
В случае установки не блокирующего режима работы компонент To8StmtOrd делает в цикле OCI вызов OCIStmtExecute до тех пор, пока он не перестанет возвращать OCI_STILL_EXECUTING. В цикле после каждой итерации вызывается событие
TOraAfterExecuteEvent = procedure(Sender: TObject; var sExec: sword) of object;
В которое по ссылке передается возвращаемое значение функции OCIStmtExecute и Вы это значение можете изменить на отличное от OCI_STILL_EXECUTING и тем самым выйти из цикла.
Вышесказанное не относится к запросам.
Вы можете принудительно прервать выполнение, вызвав метод класса To8Session
procedure BreakNonBlokinkExecution;
Использовать не блокирующий режим работы можно для нотификации пользователя о ходе выполнени какой либо процедуры, требующей длительного по времени ресурса.
Пример.
Оставьте на форме 3 компонента: To8Env, To8Session, To8StmtOrd. В To8Session взведите логическое свойство
property NonBlocking: Boolean read FNonBlocking write SetNonBlocking;
Оно должно быть TRUE.
Положите на форму еще To8Session, To8StmtOrd. To8Session свяжите с той же схемой.
Определите SQL, который будет выполняться в не блокирующем режиме:
declare
i integer;
begin
for i in 1..10000 loop
DBMS_PIPE.PACK_MESSAGE(i);
if DBMS_PIPE.SEND_MESSAGE('VLAD', 10, 8192) <> 0 then
RAISE_APPLICATION_ERROR(200000, '!!!');
end if;
end loop;
end;
Как видите, это анонимный PL-SQL блок кода, который вертит очень долгий (10000 итераций) цикл, и на каждой итерации отправляет в PIPE счетчик цикла.
Определите SQL, который будет выполняться в момент выполнения первого:
begin
if DBMS_PIPE.RECEIVE_MESSAGE('VLAD', 10) <> 0 then
raise_application_error(2000003, '!!!');
end if;
DBMS_PIPE.UNPACK_MESSAGE(:Ret);
end;
Этот PL-SQL блок кода вытаскивает из того же PIPE опубликованное первым блоком PL-SQL значение. И вытаскивает он его в переменную связи, которую я потом прочитаю объектом TOraParam.
Кстати, определите параметр Ret как ptInteger.
Второй объект класса To8StmtOrd должен быть связан со второй сессией.
У первого объекта To8StmtOrd переопределите событие OnAfterExecute:
procedure TForm10.o8StmtOrd1AfterExecute(Sender: TObject; var sExec: Integer);
begin
o8StmtOrd2.ExecSql;
Button1.Caption := o8StmtOrd2.ParamByName['Ret'].AsString;
end;
В момент выполнения первого SQL происходит выполнение второго, который вытаскивает опубликованные первым SQL значения и делает их видимыми для пользователя, помещая в Caption кнопки, которая, собственно, запускает процесс:
procedure TForm10.Button1Click(Sender: TObject);
begin
o8StmtOrd1.ExecSql;
end;
После того, как вы запустите программу и нажмете на кнопку, на самой кнопке будут перебираться цифры. Думаю, что это за цифры - еще раз объяснять не надо.
Если Вы, несмотря ни на что, дочитали-таки до этого момента, то Вы, может быть, ответите на вопрос: почему на кнопке последним значением будет не 10000?
Компонент To8DataSet является прямым потомком TDataSet, поэтому он превосходно взаимодействует со всей библиотекой VCL в части DB-aware компонентов (компонентов, зависящих от данных). Поэтому данные, предоставляемые этим компонентом, видны во всех DBGrid-ах, DBText-ах, DBImage-ах и т.д.
Он очень легко и элегантно понимается средой разработки и всеми редакторами, начиная с редактора полей TField.
По нему работают поля TField, вычисляемые поля, поля - результаты поиска (Lookup).
В него органически вделан механизм накопления изменений (cached updates), поэтому данные во всех DB-aware компонентах можно редактировать.
В общем, он ведет себя точно так же, как стандартные DataSet-ы TTable, TQuery, TStoredProc.
Дл редактирования в Design time-е предоставлены свойства:
property Active; - активизирует компонент.
property STMT: To8Stmt read FSTMT write SetFSTMT; - первоначальный источник данных для редактирования, любой наследник от To8Stmt.
property LocateType: TLocateType read FLocateType write FLocateType; - устанавливает тип поиска по командам Locate и Lookup. Может принимать значения ltLoop, ltOrdered, ltOrderedDesc, что значит поиск перебором, двоичный поиск по упорядоченному набору, двоичный поиск по упорядоченному набору в обратном порядке.
property UpdateObject: To8DataSetUpdateObject read FUpdateObject write SetUpdateObject; - указывает на объект диспетчер изменений для отправки этих изменений на сервер.
Все события для этого компонента взяты из TDataSet и ничего нового нет (смотрите документацию по TDataSet).
Дл поддержки редактирования внутри компонента живет два объекта TList, которые хранят текущие записи и удаленные записи, причем как первоначальные значени полей, так и измененный их вариант. Внутренние объекты TList заполняютс записями постепенно, по мере перемещения по выборке, причем они, собственно, берутся из объекта To8Stmt, который привязывается к DataSet-у. Таким образом, в самом DataSet-е они ДУБЛИРУЮТСЯ. Это значит, что компонент можно использовать для редактирования ЧЕЛОВЕКОМ относительно небольших выборок. Ну, например, таких, которые вообще ЧЕЛОВЕК в состоянии переварить. Использовать компонент дл просмотра огромной выборки нецелесообразно.
Картинка показывает, как взаимодействуют два компонента To8Stmt и To8DataSet:
В компоненте To8StmtOrd показано хранилище записей, разделенное на блоки по Portion записей. Когда активизируется компонент To8DataSet, он скачивает в RecordList некоторое количество записей (это количество зависит от управляющих элементов, которые на нем висят) из компонента To8StmtOrd. Компонент To8StmtOrd работает в этом случае точно так же, как работал бы в любом другом случае: он просто перемещает свою текущую запись все дальше и дальше, при необходимости делая операцию FETCH и заполняя свое хранилище Result Set.
Попавшие в RecordList записи уже можно редактировать, при этом старые их значени сохраняются. На рисунке отредактированные поля показаны красным цветом. В RecordList можно вручную добавить запись в любой момент методами TDataSet Append и Insert, на рисунке она тоже показана красным. При вызове метода Delete (удаление записи) она просто переносится в специальный TList - DeleteList. Из DeleteList запись можно (при желании) вернуть обратно.
Вот такая нехитрая, но гибкая организация работы, дающая достаточно много степеней свободы.
Ну, например, можно взять и отсортировать RecordList по какому-то признаку, Вы получите отсортированную выборку на клиенте в DataSet-е (не знаю, правда, зачем это надо). И т.д.
To8DataSet позволяет с легкостью редактировать выборку, накапливая все изменения. Но этот процесс происходит исключительно на клиенте, и сервер ничего об этом не знает. Для того, чтобы отправить изменения на сервер, их надо разделить на три потока: INSERT, DELETE, UPDATE. Причем отсеять не функциональные изменения (например, значение поля было 10, Вы его меняете на 5, а потом опять на 10). Далее открыть транзакцию, выполнить вначале все INSERT-ы с UPDATE-ами, потом DELETE-ы и закрыть транзакцию COMMIT-ом или ROLBACK-ом, в зависимости от успеха сделанных изменений.
Ответственность за выполнение большинства тех функций, которые я перечислил, лежит на компоненте To8UpdateSQL.
Вот его PUBLISHED свойства:
property ModifySQL: TStrings index 0 read GetSQLIndex write SetSQLIndex;
property InsertSQL: TStrings index 1 read GetSQLIndex write SetSQLIndex;
property DeleteSQL: TStrings index 2 read GetSQLIndex write SetSQLIndex;
Думаю, что в подробных комментариях они не нуждаются, их названия говорят сами за себя.
Однако вопрос, как заполнять эти свойства, нуждается все-таки в небольшом комментарии. Правило очень простое: если Вы делаете в любом SQL (Modify, Insert, Delete) параметр с именем поля выборки, на которую вешается объект To8UpdateSQL, то в качестве значения этого параметра будет передано НОВОЕ значение поля при сбрасывании изменений на сервер. Если значение поля не менялось, то новое значение совпадает с текущем. Если Вы делаете параметр, имя которого "OLD_" + имя поля, то значением параметра будет СТАРОЕ значение поля. Если поле не меняли, то вместо старого значения передается текущее.
Дл сохранения изменений на сервере Вам необходимо заполнить все три SQL выражения, привязать Update Object к To8DataSet-у посредством свойства UpdateObject компонента To8DataSet, сделать изменения в этом DataSet-е и вызвать метод To8DataSet.ApplyUpdates. Обращаю Ваше внимание на то, что метод ApplyUpdates Вы можете вызывать когда захотите, хоть после каждого изменения в DataSet-е.
В качестве примера давайте быстренько напишем программу для модификации таблицы EMP.
Еще раз уберите все с формы, кроме To8Env, To8Session, To8StmtOrd. SQL свойство To8StmtOrd сделайте таким:
SELECT * FROM EMP
Активизируйте To8StmtOrd. Положите на форму To8DataSet и To8UpdateSQL. To8DataSet свяжите с To8StmtOrd и с To8UpdateSQL. Установите свойства To8UpdateSQL:
DeleteSQL
delete from emp where empno = :empno
InsertSQL
insert into emp (
empno,
ename,
job,
mgr,
hiredate,
sal,
comm,
deptno
) values (
:empno,
:ename,
:job,
:mgr,
:hiredate,
:sal,
:comm,
:deptno
)
ModifySQL
update emp set
empno = :empno ,
ename = :ename ,
job = :job ,
mgr = :mgr ,
hiredate = :hiredate,
sal = :sal ,
comm = :comm ,
deptno = :deptno
where
empno = :old_empno
Как видите, операции DELETE и UPDATE проходят по старому значению первичного ключа EMPNO.
Навешайте на To8DataSet DB-aware управляющих элементов, ну хотя бы просто TDBGrid.
Активизируйте To8DataSet.
Повесьте на форму кнопку и напишите для нее реакцию:
procedure TForm11.ToolButton2Click(Sender: TObject);
begin
o8DataSet1.STMT.oraSession.TransStart;
try
o8DataSet1.ApplyUpdates;
o8DataSet1.STMT.oraSession.TransCommit;
except
o8DataSet1.STMT.oraSession.TransRollback;
end;
end;
По нажатии на эту кнопку все изменения будут передаваться на сервер.
Запустите приложение и SQL*Plus для той же схемы. Сделайте в гриде изменения, нажмите кнопку и в SQL*Plus посмотрите результат, и повторяйте, повторяйте...
To8RODataSet - еще один наследник TDataSet, со всеми вытекающими последствиями. Отличается он от To8DataSet тем, что он Read Only. У него поэтому нету свойства UpdateObject, оно ему не надо. Помимо этого, у него нет внутри никаких TList-ов, и он НЕ ДУБЛИРУЕТ нигде записи, а все запросы данных передает непосредственно объекту To8Stmt, который вытаскивает их из своего хранилища (в памяти или на диске) или делает операцию FETCH при необходимости.
Хоть этот компонент и Read Only, он не совсем Read Only. Значения его полей все-таки можно поменять. Эти изменения так же просто передаются в поля To8Stmt, а как они там изменяются, Вы уже знаете из раздела Хранение ResultSet.
Схематично можно это показать так:
Как и в предыдущем случае, красным показаны возможные изменения, которые идут непосредственно в хранилище Result Set в компоненте To8StmtOrd.
Компонент To8RODataSet я Вам рекомендую применять везде, где не требуется редактирование выборки или требуется минимальная (поставить галочку в гриде).
Очень удобен он при организации Lookup полей.
Связка To8StmtOrd + To8RODataSet при установленном свойстве ResultSetStream в rsToFile и оптимальном Portion (его я Вам рекомендую устанавливать в зависимости от размера записи выборки) дает минимальный расход оперативной памяти и максимальное быстродействие.
Я специально вынес разговор о Locate и Lookup в отдельный раздел, потому что реализованы они у меня очень оригинальным, простым способом. Вместе с тем этот способ в отдельных случаях дает производительность, НА НЕСКОЛЬКО ПОРЯДКОВ превосходящую производительность поиска в аналогичных компонентах, и в BDE в том числе.
Стандартный подход к организации поиска в DataSet-ах состоит в том, что если пользователь определяет LookUp поле (или набор полей), то в большинстве случаев значение Lookup поля ищется обычным перебором (или установкой фильтра, что, в общем, одно и то же).
В более экзотических случаях компонент строит по этому полю индекс - файл со структурой B-дерева, в котором информация упорядочена по ключу поиска. Поиск по ключу в таком файле происходит достаточно быстро, и скорость его работы зависит от параметров B-дерева (от размера страницы дерева, от количества ключей на странице, от уровня вложенности страниц в дереве). Для построения индекса требуется дополнительное время. Похоже (хотя я не уверен на 100%), что BDE работает именно таким образом, потому что поиск в очень больших выборках происходит все таки за приемлемое время.
И это хороший, вполне работоспособный подход.
Мой подход сильно все упрощает, поскольку мне очень лениво было писать классы, работающие с B-деревом (а это занятие не для слабонервных). Я подумал о том, что ORACLE сам может выдать упорядоченную выборку, и мне не надо строить никакие индексы. Пусть работает сервер, он большой и железный. А коль скоро я могу указать моему компоненту, что моя выборка упорядочена по ключу, то почему бы не сделать быстрый двоичный поиск по этому ключу? Именно это я и сделал. Дешево и сердито.
На практике все выглядит следующим образом. Давайте слегка модифицируем пример из предыдущего раздела.
Добавим к существующим на форме компонентам To8StmtOrd с select-ом:
select deptno, dname from dept ORDER BY DEPTNO
Перенаправим Result Set в файл на диске, установив свойство ResultSetStream в rsToFile, и свяжем компонент с сессией, после чего активизируем.
Добавим To8RODataSet, свяжем его с To8StmtOrd и установим свойство LocateType в значение ltOrdered. Этим мы указали компоненту, что Result Set упорядочен по интересующему нас ключу и поиск нужно производить методом половинного деления (двоичный поиск).
Теперь зайдите в редактор полей DataSet-а с таблицей EMP, добавьте все имеющиеся поля и одно поле LookUp DNAME. Установите ему ключ поиска и поле поиска DEPTNO (LookupkeyFields, KeyFields) и поле результат DNAME (LookupResultField).
После проделанных операций на экране будет:
Вы видите, что поле Lookup dname появилось в гриде. Этот пример показывает, как надо работать на принципиальном уровне, но не показывает колоссальных преимуществ в скорости работы.
Дл тестирования скорости возьмите какой-нибудь справочник, записей эдак тысяч на 50, и таблицу, использующую этот справочник, и проделайте то же самое для моих компонентов и для стандартных, а потом в гриде просто потяните скроллер вниз дл того и для другого, почувствуйте разницу!
Это очень простой компонент, исполняющий SQL скрипты, файлы со следующем содержимым:
drop table a1
/
drop table a2
/
create table a1(
f1 integer,
f2 varchar2(10)
)
/
create table a2(
f1 integer,
f2 varchar2(10)
)
/
insert into a1 values (10, 'Привет 1!')
/
insert into a1 values (20, 'Привет 2!')
/
insert into a2 values (100, 'Привет 1!')
/
insert into a2 values (200, 'Привет 2!')
Дл работы ему нужно заполнить всего два свойства:
property oraSession: To8Session read GetSession write SetSession;
property SQLScript: TStrings read GetSQLScript write SetSQLScript;
Указать сессию и, собственно говоря, скрипт.
Выполнение происходит при вызове метода
procedure ExecSQLScript;
Если в момент выполнения происходит ошибка, то высвечивается окошко с содержимым ошибки.
Сам по себе компонент ничего не выполняет, а делает лексический разбор скрипта и отправляет отдельные SQL выражения для выполнения компоненту To8StmtOrd.
Этим компонентом я решил проблему миграции данных в ORACLE. Все очень просто: указываете ему сессию, в которой нужно создать таблицу, и любой TDataSet. Жмете педаль (метод BatchMove) и чуть чуть (зависит от размера DataSet-а) ждете.
Работает со всеми типами полей, кроме ftCursor, ftDataSet, ftADT, ftArray, ftReference, ftVariant, ftInterface, ftIDispatch, ftGuid.
Работает с Calculated и Lookup полями.
Имеет свойства:
property DataSet: TDataSet read FDataSet write FDataSet; - DataSet, из которого грузятся данные.
property oraSession: To8Session read FSession write FSession; - ORACLE сессия, в которой создается таблица.
property TableName: String read FTableName write FTableName; - имя создаваемой таблицы; если не указано, то берется имя DataSet-а.
property BatchCount: Integer read FBatchCount write FBatchCount; - количество записей массива Array of structures для перекачки информации на сервер. Если в выборке есть LOB-ы, устанавливается принудительно в 1.
property CommitCount: Integer read FCommitCount write FCommitCount; - указывает, через сколько записей делать Commit.
property CreateTable: Boolean read FCreateTable write FCreateTable; - указывает, выдавать ли команду CREATE TABLE перед процессом закачки.
Имеет события:
property AfterCommit: TAfterCommit read FAfterCommit write FAfterCommit; - срабатывает после каждого COMMIT-а, передавая в обработчик общее количество зафиксированных записей.
property AfterBatch: TAfterBatch read FAfterBatch write FAfterBatch; - срабатывает после каждой отправки на сервер блока записей размером BatchCount, передавая в обработчик общее количество обработанных записей.
Как Вы, наверное, уже догадались, закачка происходит посредством передачи параметров в виде массива структур блоками по BatchCount записей, что определяет скорость работы компонента на уровне ORACLE SQL*Loader.
В случае, если в DataSet-е имеются LOB поля, BatchCount принудительно устанавливается в значение 1, но не из-за ошибки OCI, описанной в разделе Копирование ResultSet на клиента (Fetch), а вследствие конструктивных особенностей, связанных с фразой RETURNING в предложении INSERT.
В заключение хотелось бы сказать о том, чего в моих компонентах еще нет, но хотелось бы иметь, и что, скорее всего, появится в скором времени. Это работа с объектами, Nested таблицами, VarArray-ями и со ссылками на объекты, короче, со всем тем, что подразумевает объектная опция.
Я не могу сказать, что я специально откладываю эту работу. Но дело в том, что пишу компоненты главным образом для себя, а потом уже для кого-то еще. В моей работе объекты пока нужны не были, это многое объясняет. Кроме того, я считаю, что это направление в ORACLE пока сыровато. (Какие такие объекты без наследования - это скорее пользовательские структуры данных).
В общем, все это, конечно, появится, но немного надо подождать.